﻿/*
 * Taken from the .Net reference source
 */

//---------------------------------------------------------------------------
// File: SystemDropShadowChrome.cs
//
// Description:
// Implementation of system drop shadow effect.
//
// Copyright (C) 2006 by Microsoft Corporation.  All rights reserved.
//
//---------------------------------------------------------------------------

using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System;

namespace Macad.Presentation;

public sealed class DropShadow : Decorator
{
    #region Constructors

    /// <summary>
    /// Instantiates a new instance of a SystemDropShadowChrome
    /// </summary>
    public DropShadow()
    {
    }

    #endregion Constructors

    #region Dynamic Properties

    /// <summary>
    /// DependencyProperty for <see cref="Color" /> property.
    /// </summary>
    public static readonly DependencyProperty ColorProperty =
        DependencyProperty.Register(
            "Color",
            typeof(Color),
            typeof(DropShadow),
            new FrameworkPropertyMetadata(
                Color.FromArgb(0x71, 0x00, 0x00, 0x00),
                FrameworkPropertyMetadataOptions.AffectsRender,
                new PropertyChangedCallback(ClearBrushes)));

    /// <summary>
    /// The Color property defines the Color used to fill the shadow region.
    /// </summary>
    public Color Color
    {
        get { return (Color) GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }

    /// <summary>
    /// DependencyProperty for <see cref="CornerRadius" /> property.
    /// </summary>
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.Register(
            "CornerRadius",
            typeof(CornerRadius),
            typeof(DropShadow),
            new FrameworkPropertyMetadata(
                new CornerRadius(),
                FrameworkPropertyMetadataOptions.AffectsRender,
                new PropertyChangedCallback(ClearBrushes)),
            new ValidateValueCallback(IsCornerRadiusValid));

    private static bool IsCornerRadiusValid(object value)
    {
        CornerRadius cr = (CornerRadius) value;
        return !(cr.TopLeft < 0.0 || cr.TopRight < 0.0 || cr.BottomLeft < 0.0 || cr.BottomRight < 0.0 ||
                 double.IsNaN(cr.TopLeft) || double.IsNaN(cr.TopRight) || double.IsNaN(cr.BottomLeft) || double.IsNaN(cr.BottomRight) ||
                 double.IsInfinity(cr.TopLeft) || double.IsInfinity(cr.TopRight) || double.IsInfinity(cr.BottomLeft) || double.IsInfinity(cr.BottomRight));
    }

    /// <summary>
    /// The CornerRadius property defines the CornerRadius of the object casting the shadow.
    /// </summary>
    public CornerRadius CornerRadius
    {
        get { return (CornerRadius) GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }

    private static void ClearBrushes(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        ((DropShadow) o)._brushes = null;
    }

    #endregion Dynamic Properties

    #region Protected Methods

    private const double ShadowDepth = 5;

    /// <summary>
    /// Render callback.  
    /// </summary>
    protected override void OnRender(DrawingContext drawingContext)
    {
        CornerRadius cornerRadius = CornerRadius;

        Rect shadowBounds = new Rect(new Point(ShadowDepth, ShadowDepth),
                                     new Size(RenderSize.Width, RenderSize.Height));
        Color color = Color;

        if (shadowBounds.Width > 0 && shadowBounds.Height > 0 && color.A > 0)
        {
            // The shadow is drawn with a dark center the size of the shadow bounds
            // deflated by shadow depth on each side.
            double centerWidth = shadowBounds.Right - shadowBounds.Left - 2 * ShadowDepth;
            double centerHeight = shadowBounds.Bottom - shadowBounds.Top - 2 * ShadowDepth;

            // Clamp corner radii to be less than 1/2 the side of the inner shadow bounds 
            double maxRadius = Math.Min(centerWidth * 0.5, centerHeight * 0.5);
            cornerRadius.TopLeft = Math.Min(cornerRadius.TopLeft, maxRadius);
            cornerRadius.TopRight = Math.Min(cornerRadius.TopRight, maxRadius);
            cornerRadius.BottomLeft = Math.Min(cornerRadius.BottomLeft, maxRadius);
            cornerRadius.BottomRight = Math.Min(cornerRadius.BottomRight, maxRadius);

            // Get the brushes for the 9 regions
            Brush[] brushes = GetBrushes(color, cornerRadius);

            // Snap grid to device pixels
            double centerTop = shadowBounds.Top + ShadowDepth;
            double centerLeft = shadowBounds.Left + ShadowDepth;
            double centerRight = shadowBounds.Right - ShadowDepth;
            double centerBottom = shadowBounds.Bottom - ShadowDepth;

            // Because of different corner radii there are 6 potential x (or y) lines to snap to
            double[] guidelineSetX = new double[]
            {
                centerLeft,
                centerLeft + cornerRadius.TopLeft,
                centerRight - cornerRadius.TopRight,
                centerLeft + cornerRadius.BottomLeft,
                centerRight - cornerRadius.BottomRight,
                centerRight
            };

            double[] guidelineSetY = new double[]
            {
                centerTop,
                centerTop + cornerRadius.TopLeft,
                centerTop + cornerRadius.TopRight,
                centerBottom - cornerRadius.BottomLeft,
                centerBottom - cornerRadius.BottomRight,
                centerBottom
            };

            drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY));

            // The corner rectangles are drawn drawn ShadowDepth pixels bigger to 
            // account for the blur
            cornerRadius.TopLeft = cornerRadius.TopLeft + ShadowDepth;
            cornerRadius.TopRight = cornerRadius.TopRight + ShadowDepth;
            cornerRadius.BottomLeft = cornerRadius.BottomLeft + ShadowDepth;
            cornerRadius.BottomRight = cornerRadius.BottomRight + ShadowDepth;


            // Draw Top row
            Rect topLeft = new Rect(shadowBounds.Left, shadowBounds.Top, cornerRadius.TopLeft, cornerRadius.TopLeft);
            drawingContext.DrawRectangle(brushes[TopLeft], null, topLeft);

            double topWidth = guidelineSetX[2] - guidelineSetX[1];
            if (topWidth > 0)
            {
                Rect top = new Rect(guidelineSetX[1], shadowBounds.Top, topWidth, ShadowDepth);
                drawingContext.DrawRectangle(brushes[Top], null, top);
            }

            Rect topRight = new Rect(guidelineSetX[2], shadowBounds.Top, cornerRadius.TopRight, cornerRadius.TopRight);
            drawingContext.DrawRectangle(brushes[TopRight], null, topRight);

            // Middle row
            double leftHeight = guidelineSetY[3] - guidelineSetY[1];
            if (leftHeight > 0)
            {
                Rect left = new Rect(shadowBounds.Left, guidelineSetY[1], ShadowDepth, leftHeight);
                drawingContext.DrawRectangle(brushes[Left], null, left);
            }

            double rightHeight = guidelineSetY[4] - guidelineSetY[2];
            if (rightHeight > 0)
            {
                Rect right = new Rect(guidelineSetX[5], guidelineSetY[2], ShadowDepth, rightHeight);
                drawingContext.DrawRectangle(brushes[Right], null, right);
            }

            // Bottom row
            Rect bottomLeft = new Rect(shadowBounds.Left, guidelineSetY[3], cornerRadius.BottomLeft, cornerRadius.BottomLeft);
            drawingContext.DrawRectangle(brushes[BottomLeft], null, bottomLeft);

            double bottomWidth = guidelineSetX[4] - guidelineSetX[3];
            if (bottomWidth > 0)
            {
                Rect bottom = new Rect(guidelineSetX[3], guidelineSetY[5], bottomWidth, ShadowDepth);
                drawingContext.DrawRectangle(brushes[Bottom], null, bottom);
            }

            Rect bottomRight = new Rect(guidelineSetX[4], guidelineSetY[4], cornerRadius.BottomRight, cornerRadius.BottomRight);
            drawingContext.DrawRectangle(brushes[BottomRight], null, bottomRight);


            // Fill Center

            // Because the heights of the top/bottom rects and widths of the left/right rects are fixed
            // and the corner rects are drawn with the size of the corner, the center 
            // may not be a square.  In this case, create a path to fill the area

            // When the target object's corner radius is 0, only need to draw one rect
            if (cornerRadius.TopLeft == ShadowDepth &&
                cornerRadius.TopLeft == cornerRadius.TopRight &&
                cornerRadius.TopLeft == cornerRadius.BottomLeft &&
                cornerRadius.TopLeft == cornerRadius.BottomRight)
            {
                // All corners of target are 0, render one large rectangle
                Rect center = new Rect(guidelineSetX[0], guidelineSetY[0], centerWidth, centerHeight);
                drawingContext.DrawRectangle(brushes[Center], null, center);
            }
            else
            {
                // If the corner radius is TL=2, TR=1, BL=0, BR=2 the following shows the shape that needs to be created.
                //             _________________
                //            |                 |_
                //         _ _|                   |
                //        |                       |
                //        |                    _ _|
                //        |                   |   
                //        |___________________| 
                // The missing corners of the shape are filled with the radial gradients drawn above

                // Define shape counter clockwise
                PathFigure figure = new PathFigure();

                if (cornerRadius.TopLeft > ShadowDepth)
                {
                    figure.StartPoint = new Point(guidelineSetX[1], guidelineSetY[0]);
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[1], guidelineSetY[1]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[1]), true));
                }
                else
                {
                    figure.StartPoint = new Point(guidelineSetX[0], guidelineSetY[0]);
                }

                if (cornerRadius.BottomLeft > ShadowDepth)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[3]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[3]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[5]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[5]), true));
                }

                if (cornerRadius.BottomRight > ShadowDepth)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[5]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[4]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[4]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[5]), true));
                }


                if (cornerRadius.TopRight > ShadowDepth)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[2]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[2]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[0]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[0]), true));
                }

                figure.IsClosed = true;
                figure.Freeze();

                PathGeometry geometry = new PathGeometry();
                geometry.Figures.Add(figure);
                geometry.Freeze();

                drawingContext.DrawGeometry(brushes[Center], null, geometry);
            }

            drawingContext.Pop();
        }
    }

    #endregion

    #region Private Properties

    // Create common gradient stop collection for gradient brushes
    private static GradientStopCollection CreateStops(Color c, double cornerRadius)
    {
        // Scale stops to lie within 0 and 1
        double gradientScale = 1 / (cornerRadius + ShadowDepth);

        GradientStopCollection gsc = new GradientStopCollection();
        gsc.Add(new GradientStop(c, (0.5 + cornerRadius) * gradientScale));

        // Create gradient stops based on the Win32 dropshadow fall off
        Color stopColor = c;
        stopColor.A = (byte) (.74336 * c.A);
        gsc.Add(new GradientStop(stopColor, (1.5 + cornerRadius) * gradientScale));

        stopColor.A = (byte) (.38053 * c.A);
        gsc.Add(new GradientStop(stopColor, (2.5 + cornerRadius) * gradientScale));

        stopColor.A = (byte) (.12389 * c.A);
        gsc.Add(new GradientStop(stopColor, (3.5 + cornerRadius) * gradientScale));

        stopColor.A = (byte) (.02654 * c.A);
        gsc.Add(new GradientStop(stopColor, (4.5 + cornerRadius) * gradientScale));

        stopColor.A = 0;
        gsc.Add(new GradientStop(stopColor, (5 + cornerRadius) * gradientScale));

        gsc.Freeze();

        return gsc;
    }

    // Creates an array of brushes needed to render this 
    private static Brush[] CreateBrushes(Color c, CornerRadius cornerRadius)
    {
        Brush[] brushes = new Brush[9];

        // Create center brush
        brushes[Center] = new SolidColorBrush(c);
        brushes[Center].Freeze();



        // Sides
        GradientStopCollection sideStops = CreateStops(c, 0);
        LinearGradientBrush top = new LinearGradientBrush(sideStops, new Point(0, 1), new Point(0, 0));
        top.Freeze();
        brushes[Top] = top;

        LinearGradientBrush left = new LinearGradientBrush(sideStops, new Point(1, 0), new Point(0, 0));
        left.Freeze();
        brushes[Left] = left;

        LinearGradientBrush right = new LinearGradientBrush(sideStops, new Point(0, 0), new Point(1, 0));
        right.Freeze();
        brushes[Right] = right;

        LinearGradientBrush bottom = new LinearGradientBrush(sideStops, new Point(0, 0), new Point(0, 1));
        bottom.Freeze();
        brushes[Bottom] = bottom;

        // Corners

        // Use side stops if the corner radius is 0
        GradientStopCollection topLeftStops;
        if (cornerRadius.TopLeft == 0)
            topLeftStops = sideStops;
        else
            topLeftStops = CreateStops(c, cornerRadius.TopLeft);

        RadialGradientBrush topLeft = new RadialGradientBrush(topLeftStops);
        topLeft.RadiusX = 1;
        topLeft.RadiusY = 1;
        topLeft.Center = new Point(1, 1);
        topLeft.GradientOrigin = new Point(1, 1);
        topLeft.Freeze();
        brushes[TopLeft] = topLeft;

        // Reuse previous stops if corner radius is the same as side or top left
        GradientStopCollection topRightStops;
        if (cornerRadius.TopRight == 0)
            topRightStops = sideStops;
        else if (cornerRadius.TopRight == cornerRadius.TopLeft)
            topRightStops = topLeftStops;
        else
            topRightStops = CreateStops(c, cornerRadius.TopRight);

        RadialGradientBrush topRight = new RadialGradientBrush(topRightStops);
        topRight.RadiusX = 1;
        topRight.RadiusY = 1;
        topRight.Center = new Point(0, 1);
        topRight.GradientOrigin = new Point(0, 1);
        topRight.Freeze();
        brushes[TopRight] = topRight;

        // Reuse previous stops if corner radius is the same as any of the previous radii
        GradientStopCollection bottomLeftStops;
        if (cornerRadius.BottomLeft == 0)
            bottomLeftStops = sideStops;
        else if (cornerRadius.BottomLeft == cornerRadius.TopLeft)
            bottomLeftStops = topLeftStops;
        else if (cornerRadius.BottomLeft == cornerRadius.TopRight)
            bottomLeftStops = topRightStops;
        else
            bottomLeftStops = CreateStops(c, cornerRadius.BottomLeft);

        RadialGradientBrush bottomLeft = new RadialGradientBrush(bottomLeftStops);
        bottomLeft.RadiusX = 1;
        bottomLeft.RadiusY = 1;
        bottomLeft.Center = new Point(1, 0);
        bottomLeft.GradientOrigin = new Point(1, 0);
        bottomLeft.Freeze();
        brushes[BottomLeft] = bottomLeft;

        // Reuse previous stops if corner radius is the same as any of the previous radii
        GradientStopCollection bottomRightStops;
        if (cornerRadius.BottomRight == 0)
            bottomRightStops = sideStops;
        else if (cornerRadius.BottomRight == cornerRadius.TopLeft)
            bottomRightStops = topLeftStops;
        else if (cornerRadius.BottomRight == cornerRadius.TopRight)
            bottomRightStops = topRightStops;
        else if (cornerRadius.BottomRight == cornerRadius.BottomLeft)
            bottomRightStops = bottomLeftStops;
        else
            bottomRightStops = CreateStops(c, cornerRadius.BottomRight);

        RadialGradientBrush bottomRight = new RadialGradientBrush(bottomRightStops);
        bottomRight.RadiusX = 1;
        bottomRight.RadiusY = 1;
        bottomRight.Center = new Point(0, 0);
        bottomRight.GradientOrigin = new Point(0, 0);
        bottomRight.Freeze();
        brushes[BottomRight] = bottomRight;

        return brushes;
    }

    private Brush[] GetBrushes(Color c, CornerRadius cornerRadius)
    {
        if (_commonBrushes == null)
        {
            lock (_resourceAccess)
            {
                if (_commonBrushes == null)
                {
                    // Assume that the first render of DropShadow uses the most common color for the app.
                    // This breaks down if (a) the first Shadow is customized, or 
                    // (b) ButtonChrome becomes more broadly used than just on system controls.
                    _commonBrushes = CreateBrushes(c, cornerRadius);
                    _commonCornerRadius = cornerRadius;
                }
            }
        }

        if (c == ((SolidColorBrush) _commonBrushes[Center]).Color &&
            cornerRadius == _commonCornerRadius)
        {
            _brushes = null; // clear local brushes - use common
            return _commonBrushes;
        }
        else if (_brushes == null)
        {
            // need to create local brushes
            _brushes = CreateBrushes(c, cornerRadius);
        }
        return _brushes;
    }


    private const int TopLeft = 0;
    private const int Top = 1;
    private const int TopRight = 2;
    private const int Left = 3;
    private const int Center = 4;
    private const int Right = 5;
    private const int BottomLeft = 6;
    private const int Bottom = 7;
    private const int BottomRight = 8;

    // 9 brushes:
    //  0 TopLeft       1 Top       2 TopRight  
    //  3 Left          4 Center    5 Right
    //  6 BottomLeft    7 Bottom    8 BottomRight
    private static Brush[] _commonBrushes;
    private static CornerRadius _commonCornerRadius;
    private static object _resourceAccess = new object();

    // Local brushes if our color is not the common color
    private Brush[] _brushes;

    #endregion
}